package baseFx.job;

import baseFx.common.Assert;
import baseFx.common.AutoCloseable;
import baseFx.common.EventDispatcher;
import baseFx.common.concurrent.Task;
import baseFx.common.context.TypeResolver;
import baseFx.common.context.spring.FxApplicationContext;
import baseFx.common.lock.AppLockspace;
import baseFx.common.lock.Lockspace;
import baseFx.common.lock.LockspaceException;
import baseFx.common.utils.ClassUtils;
import baseFx.common.utils.ExceptionUtils;
import baseFx.common.utils.RandomUtils;
import baseFx.common.utils.StringUtils;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Properties;
import java.util.UUID;

import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;

public class TimeJob {
    private final static String rbKey = UUID.randomUUID().toString();
    private final static String raKey = UUID.randomUUID().toString();
    private final static String reKey = UUID.randomUUID().toString();

    private static FxApplicationContext resolver;
    private final static String nameKey = UUID.randomUUID().toString();
    private static Scheduler scheduler;

    public static EventDispatcher<RunBefore> before = new EventDispatcher<>(rbKey);
    public static EventDispatcher<RunAfter> after = new EventDispatcher<>(raKey);
    public static EventDispatcher<RunError> error = new EventDispatcher<>(reKey);

    public static void init(FxApplicationContext resolver) {
        init(resolver, 64);
    }

    public static void init(FxApplicationContext resolver, Integer maxThread) {
        Assert.isNull(TimeJob.resolver, "不能重复初始化");
        try {
            StdSchedulerFactory factory = new StdSchedulerFactory();
            Properties props = new Properties();
            if (maxThread != null) {
                props.put("org.quartz.threadPool.threadCount", maxThread.toString());
            }
            factory.initialize(props);
            scheduler = factory.getScheduler();
            TimeJob.resolver = resolver;
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    public static void loadFromPackage(String pkg) {
        resolver.scanPackage(pkg);
        for (Class<?> clazz : resolver.getClassListForType(FxJob.class)) {
            JobComponent attr = clazz.getDeclaredAnnotation(JobComponent.class);
            if (attr != null) {
                start((Class<? extends FxJob>) clazz, attr.seconds());
            }
        }
    }

    public static void start(Class<? extends FxJob> clazz, int second) {
        try {
            SimpleScheduleBuilder builder = simpleSchedule().withIntervalInSeconds(second).repeatForever();
            String id = UUID.randomUUID().toString();
            Trigger trigger = newTrigger().withIdentity(id, "jobs").startNow().withSchedule(builder)
                    .build();
            JobDetail job = JobBuilder.newJob(Runner.class).withIdentity(id, "jobs")
                    .build();
            job.getJobDataMap().put(nameKey, clazz.getCanonicalName());
            scheduler.scheduleJob(job, trigger);
            if (scheduler.isStarted() == false) {
                scheduler.start();
            }
        } catch (SchedulerException e) {
            throw new RuntimeException(clazz.getSimpleName(), e);
        }
    }

    @DisallowConcurrentExecution
    public static class Runner implements Job {
        private Class<?> jobClass;
        private String lockey;
        private Lockspace lockspace;
        private Integer delayMillis;

        private void init(JobExecutionContext jobContext) {
            if (jobClass != null) {
                return;
            }
            String name = jobContext.getJobDetail().getJobDataMap().getString(nameKey);
            jobClass = ClassUtils.getClassForName(name);
            if (jobClass == null) {
                stopJob(jobContext);
                throw new RuntimeException(name + " 加载失败");
            }
            JobComponent setting = jobClass.getAnnotation(JobComponent.class);
            if (setting != null) {
                if ("true".equalsIgnoreCase(setting.single()) || StringUtils.isNullOrEmpty(setting.single())) {
                    lockey = StringUtils.join("-", "timeJob", jobClass.getSimpleName(), jobClass.getCanonicalName().hashCode());
                } else if ("false".equalsIgnoreCase(setting.single()) == false) {
                    lockey = setting.single();
                }
                if (setting.precise() == false) {
                    delayMillis = Integer.parseInt(RandomUtils.numByRange(0, 5000));
                }
            }
        }

        @Override
        public void execute(JobExecutionContext jobContext) {
            init(jobContext);
            if (delayMillis != null && delayMillis > 0) {
                Task.delay(delayMillis);
            }
            try (TypeResolver ctx = resolver.sub()) {
                AutoCloseable ac = null;
                FxJob job = null;
                try {
                    job = (FxJob) ctx.resolve(jobClass);
                    if (lockey != null) {
                        if (lockspace == null) {
                            try {
                                lockspace = ctx.resolve(Lockspace.class);
                            } catch (Exception e) {
                                lockspace = new AppLockspace();
                            }
                        }
                        ac = lockspace.exLock(lockey);
                    }
                    RunBefore runBefore = new RunBefore(jobClass);
                    before.fire(rbKey, jobContext, runBefore);
                    if (runBefore.cancel) {
                        return;
                    }
                    job.run();
                    RunAfter runAfter = new RunAfter(jobClass);
                    after.fire(raKey, jobContext, runAfter);
                } catch (LockspaceException e) {
                    job.locked();
                } catch (Exception e) {
                    if (e instanceof JobStopException || job == null) {
                        throw e;
                    }
                    Exception rs = job.onError(e);
                    if (rs != null) {
                        RunError runError = new RunError(jobClass, rs);
                        error.fire(reKey, jobContext, runError);
                        if (runError.error instanceof JobStopException) {
                            throw new JobStopException();
                        } else if (runError.error != null) {
                            throw ExceptionUtils.toRuntime(runError.error);
                        }
                    }
                } finally {
                    if (ac != null) {
                        ac.close();
                    }
                }
            } catch (JobStopException e) {
                stopJob(jobContext);
            }
        }

        private void stopJob(JobExecutionContext jobContext) {
            try {
                Scheduler scheduler = jobContext.getScheduler();
                scheduler.pauseTrigger(jobContext.getTrigger().getKey());
                scheduler.unscheduleJob(jobContext.getTrigger().getKey());
                scheduler.deleteJob(jobContext.getJobDetail().getKey());
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}
